﻿using System;
using System.Data;
using System.Text;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.Globalization;

using BizElements.Core.ProviderComponents;

namespace BizElements.Core
{
    #region Documentation.
    /// <summary>
    /// Contains relations and additional information required to build a complete join path.
    /// </summary>
    /// <remarks><para>Data-access tier objects use RelationBucket to automatically build a FROM clause.
    /// Algorithm ensures that joins are always rendered in the the correct order regardles of the order 
    /// in which the relations have been added to the bucket. Outer joins are propagated if necessary.</para>
    /// <para>RelationBucket also provides an easy way to create complex <see cref="DataSet"/> objects 
    /// (see <see cref="NewDataSet()"/>).</para></remarks>
    /// <example>
    /// The following example creates a RelationBucket object that connects Employees, EmployeeTerritories, Territories and Region tables.
    /// <code>
    /// EmployeesMeta employees = new EmployeesMeta();
    /// EmployeeTerritoriesMeta empTerritories = new EmployeeTerritoriesMeta();
    /// TerritoriesMeta territories = new TerritoriesMeta();
    /// RegionMeta regions = new RegionMeta();
    /// 
    /// RelationBucket relations = new RelationBucket();
    /// relations.Add(employees, empTerritories, empTerritories.EmployeeID);
    /// relations.Add(territories, empTerritories, empTerritories.TerritoryID);
    /// relations.Add(regions, territories, territories.RegionID);
    /// </code>
    /// </example>
    #endregion
    [Serializable]
    public sealed class RelationBucket
    {
        #region Members.

        /// <summary>
        /// Relations.
        /// </summary>
        DbRelationCollection relations = new DbRelationCollection();

        /// <summary>
        /// Additional JOIN info (inner/outer parent/child) for each relation.
        /// Indexes of the join modes equal the relation indices.
        /// </summary>
        List<JoinMode> joinModes = new List<JoinMode>();

        #endregion

        #region Constructors.

        /// <summary>
        /// Initializes a new instance of the RelationBucket class.
        /// </summary>
        public RelationBucket()
        {
        }

        /// <summary>
        /// Initializes a new instance of the RelationBucket class.
        /// </summary>
        /// <param name="firstRelation">First/initial relation added to the bucket using <see cref="Add(DbRelation)"/> method.</param>
        public RelationBucket(DbRelation firstRelation)
        {
            Add(firstRelation);
        }

        /// <summary>
        /// Initializes a new instance of the RelationBucket class and adds an initial relation to the bucket using <see cref="Add(DbRelation)"/> method.
        /// </summary>	
        /// <param name="parent">Parent/master table.</param>
        /// <param name="child">Child/data table.</param>
        /// <param name="childForeignKey">Child foreign key.</param>
        public RelationBucket(IDbTable parent, IDbTable child, IDbColumn childForeignKey)
        {
            Add(parent, child, childForeignKey);
        }

        /// <summary>
        /// Initializes a new instance of the RelationBucket class.
        /// </summary>
        /// <param name="relations">Relations to add to the bucket using <see cref="Add(DbRelation)"/> method.</param>
        public RelationBucket(params DbRelation[] relations)
        {
            foreach (DbRelation rel in relations)
                Add(rel);
        }

        #endregion

        #region Add.

        /// <summary>
        /// Adds a relation to the bucket.
        /// </summary>
        /// <remarks>If parallel relations are used then tables must use aliases, otherwise an error will occur.</remarks>
        /// <param name="relation">Relation.</param>
        /// <param name="parentAsOuter">Specifies whether the parent table is connected as outer.</param>
        /// <param name="childrenAsOuter">Specifies whether the child table is connected as outer.</param>
        /// <remarks>Child table is connected as outer. Parent table is connected as outer if foreign 
        /// key is made of exactly one nullable column. If parallel relations are used then tables must 
        /// use aliases, otherwise an error will occur.</remarks>
        /// <exception cref="ArgumentException">is generated if the bucket already contains a relation
        /// which contains the same tables with the same aliases as the new relation.</exception>
        public void Add(DbRelation relation, bool parentAsOuter, bool childrenAsOuter)
        {
            EnsureTheRelationIsNotAlreadyInTheBucket(relation);
            this.relations.Add(relation);
            this.joinModes.Add(new JoinMode(parentAsOuter, childrenAsOuter));
        }

        /// <summary>
        /// Adds a relation to the bucket.
        /// </summary>
        /// <remarks>If parallel relations are used then tables must use aliases, otherwise an error will occur.</remarks>
        /// <param name="relation">Relation.</param>
        /// <param name="parentAsOuter">Specifies whether the parent table is connected as outer.</param>
        /// <remarks>Child table is connected as outer. Parent table is connected as outer if foreign 
        /// key is made of exactly one nullable column. If parallel relations are used then tables must 
        /// use aliases, otherwise an error will occur.</remarks>
        /// <exception cref="ArgumentException">is generated if the bucket already contains a relation
        /// which contains the same tables with the same aliases as the new relation.</exception>
        public void Add(DbRelation relation, bool parentAsOuter)
        {
            EnsureTheRelationIsNotAlreadyInTheBucket(relation);
            this.relations.Add(relation);
            this.joinModes.Add(new JoinMode(parentAsOuter, /*children not outer*/false));
        }

        /// <summary>
        /// Adds a relation to the bucket. 
        /// </summary>
        /// <param name="relation">Relation.</param>
        /// <remarks>Child table is connected as outer. Parent table is connected as outer if foreign 
        /// key is made of exactly one nullable column. If parallel relations are used then tables must 
        /// use aliases, otherwise an error will occur.</remarks>
        /// <exception cref="ArgumentException">is generated if the bucket already contains a relation
        /// which contains the same tables with the same aliases as the new relation.</exception>
        public void Add(DbRelation relation)
        {
            EnsureTheRelationIsNotAlreadyInTheBucket(relation);
            this.relations.Add(relation);
            bool parentAsOuter = IsNullableFk(relation.ChildForeignKey);
            this.joinModes.Add(new JoinMode(parentAsOuter, true));
        }

        private static bool IsNullableFk(IDbColumn[] fk)
        {
            foreach (IDbColumn fkPart in fk)
            {
                if (fkPart.IsNullable)
                    return true;
            }

            return false;
        }

        /// <summary>
        /// Adds a relation to the bucket.
        /// </summary>
        /// <param name="parent">Parent/master table.</param>
        /// <param name="child">Child/data table.</param>
        /// <param name="childForeignKey">Child foreign key.</param>
        /// <remarks>Child table is connected as outer. Parent table is connected as outer if foreign 
        /// key is made of exactly one nullable column. If parallel relations are used then tables must 
        /// use aliases, otherwise an error will occur.</remarks>
        /// <exception cref="ArgumentException">is generated if the bucket already contains a relation
        /// which contains the same tables with the same aliases as the new relation.</exception>
        public void Add(IDbTable parent, IDbTable child, IDbColumn childForeignKey)
        {
            DbRelation relation = new DbRelation(parent, child, childForeignKey);
            Add(relation);
        }

        private void EnsureTheRelationIsNotAlreadyInTheBucket(DbRelation newRelation)
        {
            bool alreadyInTheBucket = (this.relations[newRelation.Parent, newRelation.Child] != null);
            if (alreadyInTheBucket)
            {
                //throw new InvalidOperationException("The bucket already contains a relation that connects the " + newRelation.Parent.Alias + " and " + newRelation.Child.Alias + " tables.");
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Messages.RelationBucket_AlreadyConnectsXAndYTables, newRelation.Parent.Alias, newRelation.Child.Alias));
            }
        }

        #endregion

        #region Merge.

        /// <summary>
        /// Merges a specified RelationBucket object into the current RelationBucket so that there are no duplicate relations.
        /// </summary>
        /// <param name="bucket">The RelationBucket that will be merged.</param>
        public void Merge(RelationBucket bucket)
        {
            for (int relationIdx = 0; relationIdx < bucket.relations.Count; relationIdx++)
            {
                DbRelation newRelation = bucket.relations[relationIdx];
                JoinMode newJoinMode = (JoinMode)bucket.joinModes[relationIdx];

                DbRelation existingEqualRelation = this.relations[newRelation.Parent, newRelation.Child];
                if (existingEqualRelation == null)
                {
                    this.relations.Add(newRelation);
                    this.joinModes.Add(newJoinMode);
                }
            }
        }

        #endregion

        #region DataSet conversions.

        /// <summary>
        /// Creates a new empty <see cref="DataSet"/> with the table structure, relations and constraints defined in the RelationBucket.
        /// </summary>
        /// <returns>New DataSet.</returns>
        /// <example>
        /// The following example create a new DataSet that contains four tables, relations between them and all constraints.
        /// <code>
        /// public DataSet CreateDataSet()
        /// {
        /// 	EmployeesMeta employees = new EmployeesMeta();
        /// 	EmployeeTerritoriesMeta empTerritories = new EmployeeTerritoriesMeta();
        /// 	TerritoriesMeta territories = new TerritoriesMeta();
        /// 	RegionMeta regions = new RegionMeta();
        /// 
        /// 	// Create relations between tables.
        /// 	RelationBucket relations = new RelationBucket();
        /// 	relations.Add(employees, empTerritories, empTerritories.EmployeeID);
        /// 	relations.Add(territories, empTerritories, empTerritories.TerritoryID);
        /// 	relations.Add(regions, territories, territories.RegionID);
        /// 
        /// 	// Create a DataSet object with relations between DataTables and constraints 
        /// 	// (primary key, foreign key and unique constraints.
        /// 	DataSet ds = relations.NewDataSet();
        /// 	return ds;
        /// }
        /// </code>
        /// </example>
        public DataSet NewDataSet()
        {
            return NewDataSet(null, true, true, true);
        }

        /// <summary>
        /// Creates a new empty <see cref="DataSet"/> with the table structure, relations and constraints defined in the RelationBucket.
        /// </summary>
        /// <param name="dataSetName">The name of the DataSet.</param>
        /// <param name="createPrimaryKeys">A value indicating whether to create primary keys.</param>
        /// <param name="createRelations">A value indicating whether to create <see cref="DataRelation"/> objects.</param>
        /// <param name="createConstraints">A value indicating whether to create constraints.</param>
        /// <returns>New DataSet.</returns>
        /// <example>
        /// The following example create a new DataSet that contains four tables, relations between them and all constraints.
        /// <code>
        /// public DataSet CreateDataSet()
        /// {
        /// 	EmployeesMeta employees = new EmployeesMeta();
        /// 	EmployeeTerritoriesMeta empTerritories = new EmployeeTerritoriesMeta();
        /// 	TerritoriesMeta territories = new TerritoriesMeta();
        /// 	RegionMeta regions = new RegionMeta();
        /// 
        /// 	// Create relations between tables.
        /// 	RelationBucket relations = new RelationBucket();
        /// 	relations.Add(employees, empTerritories, empTerritories.EmployeeID);
        /// 	relations.Add(territories, empTerritories, empTerritories.TerritoryID);
        /// 	relations.Add(regions, territories, territories.RegionID);
        /// 
        /// 	// Create a DataSet object with relations between DataTables and constraints 
        /// 	// (primary key, foreign key and unique constraints.
        /// 	DataSet ds = relations.NewDataSet("EmployeeData", true, true, true);
        /// 	return ds;
        /// }
        /// </code>
        /// </example>
        public DataSet NewDataSet(string dataSetName, bool createPrimaryKeys, bool createRelations, bool createConstraints)
        {
            DataSet ds = (dataSetName != null)
                ? new DataSet(dataSetName) { Locale = CultureInfo.InvariantCulture }
                : new DataSet() { Locale = CultureInfo.InvariantCulture };

            StringCollection alreadyCreatedTables = new StringCollection();
            foreach (DbRelation relation in this.relations)
            {
                // Create required ADO.NET objects: DataTables, primary keys, relations, constraints (FK, unique).				
                DataTable parent = ds.Tables[relation.Parent.Alias];
                if (parent == null)
                {
                    parent = relation.Parent.NewDataTable();
                    ds.Tables.Add(parent);
                    alreadyCreatedTables.Add(relation.Parent.Alias);
                }

                DataTable child = ds.Tables[relation.Child.Alias];
                if (child == null)
                {
                    child = relation.Child.NewDataTable();
                    ds.Tables.Add(child);
                    alreadyCreatedTables.Add(relation.Child.Alias);
                }

                DataColumn[] parentPk = new DataColumn[relation.Parent.PrimaryKey.Count];
                for (int parentPkFieldIdx = 0; parentPkFieldIdx < parentPk.Length; parentPkFieldIdx++)
                    parentPk[parentPkFieldIdx] = parent.Columns[relation.Parent.PrimaryKey[parentPkFieldIdx].ColumnName];

                DataColumn[] childPk = new DataColumn[relation.Child.PrimaryKey.Count];
                for (int childPkFieldIdx = 0; childPkFieldIdx < childPk.Length; childPkFieldIdx++)
                    childPk[childPkFieldIdx] = child.Columns[relation.Child.PrimaryKey[childPkFieldIdx].ColumnName];

                DataColumn[] childFk = new DataColumn[relation.ChildForeignKey.Length];
                for (int childFkFieldIdx = 0; childFkFieldIdx < childFk.Length; childFkFieldIdx++)
                    childFk[childFkFieldIdx] = child.Columns[relation.ChildForeignKey[childFkFieldIdx].ColumnName];

                // Add created object to DataSet.
                if (alreadyCreatedTables.Contains(parent.TableName) == false)
                {
                    if (createPrimaryKeys)
                        parent.PrimaryKey = parentPk;

                    ds.Tables.Add(parent);
                    alreadyCreatedTables.Add(parent.TableName);
                }

                if (alreadyCreatedTables.Contains(child.TableName) == false)
                {
                    if (createPrimaryKeys)
                        child.PrimaryKey = childPk;

                    ds.Tables.Add(child);
                    alreadyCreatedTables.Add(child.TableName);
                }

                if (createRelations)
                {
                    DataRelation adoRelation = new DataRelation(relation.Name, parentPk, childFk, createConstraints);
                    ds.Relations.Add(adoRelation);
                }
            }

            return ds;
        }

        /// <summary>
        /// Creates ADO.NET constraints on the target <see cref="DataSet"/>.
        /// </summary>
        /// <param name="targetDataSet">DataSet to which the constraints are to be applied.</param>
        /// <param name="createPrimaryKeys">A value indicating whether to create primary keys.</param>
        /// <param name="createRelations">A value indicating whether to create <see cref="DataRelation"/> objects.</param>
        /// <param name="createConstraints">A value indicating whether to create constraints.</param>
        /// <remarks><para>DataSet needs to have the same table structure as the RelationBucket; otherwise an
        /// Exception is generated.</para>
        /// <para>The method doesn't check if the DataSet already contains any constraints. An error might occur 
        /// when applying duplicate constraints.</para></remarks>
        public void CreateDataSetConstraints(DataSet targetDataSet, bool createPrimaryKeys, bool createRelations, bool createConstraints)
        {
            StringCollection alreadyProcessedTables = new StringCollection();
            foreach (DbRelation relation in this.relations)
            {
                // Create required ADO.NET objects: DataTables, primary keys, relation, constraints (FK, unique).				
                DataTable parent = targetDataSet.Tables[relation.Parent.Alias];
                if (parent == null)
                    throw new ArgumentException(Messages.RelationBucket_DataSetNotCompatible + relation.Parent.Alias, "targetDataSet");

                DataTable child = targetDataSet.Tables[relation.Child.Alias];
                if (child == null)
                    throw new ArgumentException(Messages.RelationBucket_DataSetNotCompatible + relation.Child.Alias, "targetDataSet");

                DataColumn[] parentPk = new DataColumn[relation.Parent.PrimaryKey.Count];
                for (int parentPkFieldIdx = 0; parentPkFieldIdx < parentPk.Length; parentPkFieldIdx++)
                    parentPk[parentPkFieldIdx] = parent.Columns[relation.Parent.PrimaryKey[parentPkFieldIdx].ColumnName];

                DataColumn[] childPk = new DataColumn[relation.Child.PrimaryKey.Count];
                for (int childPkFieldIdx = 0; childPkFieldIdx < childPk.Length; childPkFieldIdx++)
                    childPk[childPkFieldIdx] = child.Columns[relation.Child.PrimaryKey[childPkFieldIdx].ColumnName];

                DataColumn[] childFk = new DataColumn[relation.ChildForeignKey.Length];
                for (int childFkFieldIdx = 0; childFkFieldIdx < childFk.Length; childFkFieldIdx++)
                    childFk[childFkFieldIdx] = child.Columns[relation.ChildForeignKey[childFkFieldIdx].ColumnName];                

                // Add created object to DataSet.
                if (alreadyProcessedTables.Contains(parent.TableName) == false)
                {
                    if (createPrimaryKeys)
                        parent.PrimaryKey = parentPk;

                    alreadyProcessedTables.Add(parent.TableName);
                }

                if (alreadyProcessedTables.Contains(child.TableName) == false)
                {
                    if (createPrimaryKeys)
                        child.PrimaryKey = childPk;

                    alreadyProcessedTables.Add(child.TableName);
                }

                if (createRelations)
                {
                    DataRelation adoRelation = new DataRelation(relation.Name, parentPk, childFk, createConstraints);
                    targetDataSet.Relations.Add(adoRelation);
                }
            }
        }

        #endregion

        #region Properties.

        /// <summary>
        /// Gets the number of relations in the bucket.
        /// </summary>
        public int Count
        {
            get { return this.relations.Count; }
        }

        /// <summary>
        /// Gets the <see cref="DbRelation"/> objects contained in the bucket.
        /// </summary>
        public DbRelation[] Relations
        {
            get { return this.relations.ToArray(); }
        }

        /// <summary>
        /// Gets the <see cref="IDbTable"/> objects connected by relations in this bucket.
        /// </summary>
        public IDbTable[] Tables
        {
            get { return this.relations.Tables; }
        }

        #endregion

        #region Clone.

        /// <summary>
        /// Creates a deep copy of current instance.
        /// </summary>
        /// <returns>DbRelationBucket that is a coppy of current instance.</returns>
        public RelationBucket Clone()
        {
            RelationBucket clonedBucket = new RelationBucket();
            for (int i = 0; i < this.relations.Count; i++)
            {
                DbRelation currRelation = this.relations[i];
                DbRelation clonedRelation = currRelation.Clone(currRelation.Child.ColumnAliasesArePrefixed, currRelation.Parent.ColumnAliasesArePrefixed);
                JoinMode mode = this.joinModes[i];
                clonedBucket.Add(clonedRelation, mode.ParentAsOuter, mode.ChildrenAsOuter);
            }

            return clonedBucket;
        }

        #endregion

        #region FROM clause rendering - rendering extracted to dedicated class, sorting/path algorithm stil here.

        /// <summary>
        /// Renders SQL FROM clause starting with the specified table.
        /// Automatically determines the correct order of JOIN clauses and modes (inner/left outer) for all tables.
        /// </summary>
        /// <param name="firstTable">1st table in the FROM clause.</param>
        /// <param name="dbms">DBMS. Required because table names with special characters are handled differently on different DBMSs.</param>
        /// <param name="output">StringBuilder to which SQL is appended.</param>
        /// <returns>FROM clause.</returns>
        public void RenderFromClause(IDbTable firstTable, DbmsType dbms, StringBuilder output)
        {
            // For each table.
            //		Create empty ordered 'from tables' and relations list.
            //		Lists are filled with all tables and relations in the matching order which enables the creation of FromClause.
            //
            //		Add current table to the beginning of the 'from' list.
            //
            //		(1) Get 1st relation wich connects one of the tables already in the 'from' list to a new table.
            //		If such relation exists.
            //			Add to ordered relation list.
            //			Add new table to the from list.
            //			If the new table needs to be connected with 'left outer join' then set its JoinAsOuter flag.
            //			Continue at step (1)

            // Algorithm assumes that the first table is the most important one.
            // If the entity in that table is null then values from other tables will be null as well.
            // This means that right outer joins are never used.
            // Algorithm builds the from statement beginning with the specified table and all the other tables
            // are connected by inner or left outer joins.
            // Note: even though DbRelation objects contain all the required information to build 
            // right outer joins that is never done.			

            DbRelation[] orderedRelations;
            TableWithJoinMode[] orderedFromTables;
            SortRelationsAndJoinModes(this.relations, this.joinModes, firstTable, out orderedRelations, out orderedFromTables);
            ThrowExceptionIfNotAllTablesAreConnected(orderedFromTables);
            
            FromClause from = new FromClause(firstTable, orderedRelations, orderedFromTables);
            IFromClauseRenderer renderer = DbmsComponentFactory.GetComponent<IFromClauseRenderer>(dbms);
            renderer.Render(from, dbms, output);
        }

        private static void SortRelationsAndJoinModes(DbRelationCollection allRelations, List<JoinMode> allJoinModes, IDbTable firstTable, out DbRelation[] orderedRelations, out TableWithJoinMode[] orderedFromTables)
        {
            // Sorts relations in a fashion so that only INNER and LEFT OUTER joins are used. RIGHT joins will never be required.

            orderedRelations = new DbRelation[allRelations.Count];
            int relationIdx = 0;
            orderedFromTables = new TableWithJoinMode[allJoinModes.Count + 1];
            int fromIdx = 0;

            // JoinAsOuter property doesn't doesn't affect 1st table.
            orderedFromTables[fromIdx] = new TableWithJoinMode(firstTable, false);
            fromIdx++;

            DbRelation nextRelation;
            TableWithJoinMode nextTable;
            while (GetNextRelationAndTable(allRelations, allJoinModes, orderedFromTables, out nextRelation, out nextTable))
            {
                orderedRelations[relationIdx++] = nextRelation;
                orderedFromTables[fromIdx++] = nextTable;
            }
        }

        private static bool GetNextRelationAndTable(DbRelationCollection allRelations, List<JoinMode> allJoinModes, TableWithJoinMode[] usedTables, out DbRelation nextRelation, out TableWithJoinMode nextTable)
        {
            nextRelation = null;
            nextTable = null;
            for (int idxRelation = 0; idxRelation < allRelations.Count; idxRelation++)
            {
                DbRelation currentRelation = allRelations[idxRelation];

                IDbTable newTableCandidate = null;
                TableWithJoinMode alreadyUsedTableFromCurrentRelation = null;
                bool connectAsOuter = false;
                int numOfTablesCurrentRelationConnects = 0;

                // Check how many of the used tables current relation connects.
                for (int idxTable = 0; idxTable < usedTables.Length; idxTable++)
                {
                    if (usedTables[idxTable] == null)
                        continue;

                    if (currentRelation.Parent.HasEqualAliasAndNameAs(usedTables[idxTable].Table))
                    {
                        // Parent table is already in the ordered from tables list.

                        numOfTablesCurrentRelationConnects++;
                        newTableCandidate = currentRelation.Child;
                        alreadyUsedTableFromCurrentRelation = usedTables[idxTable];
                        if (allJoinModes[idxRelation].ChildrenAsOuter == true)
                            connectAsOuter = true;
                    }

                    if (currentRelation.Child.HasEqualAliasAndNameAs(usedTables[idxTable].Table))
                    {
                        // Child table is already in the ordered from tables list.

                        numOfTablesCurrentRelationConnects++;
                        newTableCandidate = currentRelation.Parent;
                        alreadyUsedTableFromCurrentRelation = usedTables[idxTable];
                        if (allJoinModes[idxRelation].ParentAsOuter == true)
                            connectAsOuter = true;
                    }
                }

                // If current relation connects only one used table than the other one is a new table.				
                // Return the new relation and table.
                if (numOfTablesCurrentRelationConnects == 1)
                {
                    // If the already used table is connected as outer then the new one MUST be outer, too.
                    if (alreadyUsedTableFromCurrentRelation.JoinAsOuter == true)
                        connectAsOuter = true;

                    nextRelation = currentRelation;
                    nextTable = new TableWithJoinMode(newTableCandidate, connectAsOuter);
                    return true;
                }
            }

            return false;
        }

        private static void ThrowExceptionIfNotAllTablesAreConnected(TableWithJoinMode[] orderedFromTables)
        {
            // Make sure that all tables are connected.
            foreach (TableWithJoinMode joinedTable in orderedFromTables)
            {
                if (joinedTable == null)
                {
                    string errorMessage = GenerateRelationsDontConnectAllTablesErrorMessage(orderedFromTables);
                    throw new InvalidOperationException(errorMessage);
                }
            }
        }

        private static string GenerateRelationsDontConnectAllTablesErrorMessage(TableWithJoinMode[] orderedFromTables)
        {
            string error = Messages.RelationBucket_NotAllTablesAreConnectedTheseAreConnected;
            string connectedTables = "";
            foreach (TableWithJoinMode table in orderedFromTables)
            {
                if (table != null)
                    connectedTables += " " + table.Table.Alias + ",";
            }
            if (string.IsNullOrEmpty(connectedTables))
                error += " " + Messages.RelationBucket_None;
            else
                error += connectedTables.TrimEnd(',');
            error += ".";
            return error;
        }

        #endregion
    }

    /// <summary>Contains a table and a flag which specifies whether it is outer joined. Requiried for rendering algorithm.</summary>
    internal sealed class TableWithJoinMode
    {
        #region Public fields - no encapsulation.

        /// <summary>
        /// Table.
        /// </summary>
        public IDbTable Table;

        /// <summary>
        /// Specifies whether a table is outer joined.
        /// </summary>
        public bool JoinAsOuter = false;

        #endregion

        #region CTor.

        public TableWithJoinMode(IDbTable table, bool joinAsOuter)
        {
            this.Table = table;
            this.JoinAsOuter = joinAsOuter;
        }

        #endregion
    }

    /// <summary>Defines wheter parent or child table is joined as outer.</summary>
    [Serializable]
    internal struct JoinMode
    {
        #region Public fields - no encapsulation.

        /// <summary>
        /// Specifies whether the parent table is connected as outer.
        /// </summary>
        public bool ParentAsOuter;

        /// <summary>
        /// Specifies whether the child table is connected as outer.
        /// </summary>
        public bool ChildrenAsOuter;

        #endregion

        #region CTor.

        public JoinMode(bool parentAsOuter, bool childrenAsOuter)
        {
            this.ParentAsOuter = parentAsOuter;
            this.ChildrenAsOuter = childrenAsOuter;
        }

        #endregion
    }    
}